-module(erl_sql_compare_parse).
-feature(maybe_expr, enable).
-author("zhuiyi9009@163.com").

-include("common.hrl").

%% API
-export([test/0]).
-export([parse_from_file/2,
         parse_from_mysql/2]).

test() ->
    test_parse_index(),
    ok.

parse_from_file(Schema, File) ->
    case file:read_file(File) of
        {ok, Binary} ->
            try
                {ok, Pattern} = re:compile("CREATE\\s+TABLE\\s*`(\\w+)`\\s*\\((.+?)\\)\\s*ENGINE.*?;", [dotall]),
                SchemaStringList =
                    case re:run(Binary, Pattern, [global, {capture, all, list}]) of
                        nomatch ->
                            case re:run(Binary, "(CREATE|create)", [{capture, none}]) of
                                nomatch ->
                                    [];
                                _ ->
                                    %% something maybe wrong
                                    throw("not match create table statement")
                            end;
                        {match, Captured} ->
                            Captured
                    end,
                SchemaList = parse_schema_list(SchemaStringList),
                {ok, Schema#schema_sync{table_list = SchemaList, type = file}}
            catch
                Err:Reason:Stack ->
                    ?WARN("Err: ~p, Reason: ~p~n, Stack: ~w~n", [Err, Reason, Stack]),
                    {error, Reason}
            end;
        {error, Reason} ->
            {error, Reason}
    end.

parse_from_mysql(Schema, Dsn) ->
    maybe
        PoolId = erl_sql_compare_db:pool_id(Schema),
        ok ?= erl_sql_compare_db:init_db(PoolId, Dsn),
        {ok, Rows} ?= erl_sql_compare_db:fetch(PoolId, "SHOW TABLES;"),
        {ok, Pattern} = re:compile("CREATE\\s+TABLE.*?\\((.+?)\\)\\s*ENGINE.*?;", [dotall]),
        TableSchemaList =
            [begin
                 TableString = binary_to_list(Table),
                 Sql = io_lib:format("SHOW CREATE TABLE ~s", [TableString]),
                 {ok, [[_, CreateSqlBin]]} = erl_sql_compare_db:fetch(PoolId, Sql),
                 CreateSql = string:trim(binary_to_list(CreateSqlBin)) ++ ";",

                 %% 获取field详情
                 case re:run(CreateSql, Pattern, [{capture, all_but_first, list}]) of
                     nomatch ->
                         {error, re_pattern_nomatch};
                     {match, [FieldStrings]} ->
                         parse_schema(#table_schema{table = TableString,
                                                    sql = CreateSql},
                                      FieldStrings)
                 end
             end || [Table] <- Rows],
        {ok, Schema#schema_sync{table_list = TableSchemaList, type = mysql}}
    end.

parse_schema_list(SchemaStringList) ->
    [begin
         %% windows compatible
         Sql2 = string:replace(string:trim(Sql), "\r\n", "\n", 'all'),
         parse_schema(#table_schema{table = TableName,
                                    sql = string:trim(Sql2)}, FieldStrings)
     end || [Sql, TableName, FieldStrings] <- SchemaStringList].

parse_schema(#table_schema{table = TableName} = Schema, FieldStrings) ->
    Lines = string:tokens(FieldStrings, "\n"),
    #table_schema{field_list = Fields2,
                  index_list = IndexList2} = Schema2 =
        lists:foldl(
            fun(Line, #table_schema{field_list = Fields,
                                    index_list = IndexList} = AccSchema) ->
                case string:trim(Line) of
                    "" ->
                        AccSchema;
                    Line2 ->
                        case string:trim(Line2, 'trailing', ",") of
                            "`" ++ Rest = Line3 ->
                                %% 字段
                                case string:chr(Rest, $`) of
                                    0 ->
                                        throw({error_table_field, TableName, Line});
                                    1 ->
                                        throw({error_table_field, TableName, Line});
                                    Index ->
                                        Name = string:substr(Rest, 1, Index - 1),
                                        FieldInfo = #table_field{name = Name, sql = Line3},
                                        AccSchema#table_schema{field_list = [FieldInfo | Fields]}
                                end;
                            Line3 ->
                                case parse_index(Line3) of
                                    #table_index{} = Index ->
                                        AccSchema#table_schema{index_list = [Index | IndexList]};
                                    {error, Err} ->
                                        throw({Err, TableName, Line});
                                    Other ->
                                        throw({not_match, Other, TableName, Line})
                                end
                        end
                end
            end, Schema, Lines),
    Schema2#table_schema{field_list = lists:reverse(Fields2),
                         index_list = lists:reverse(IndexList2)}.

parse_index(Line) ->
    %% UNIQUE KEY `idx_a` (`a`) USING HASH COMMENT '注释',
    %% FULLTEXT KEY `c` (`c`)
    %% PRIMARY KEY (`d`)
    %% KEY `idx_e` (`e`),
    %% CONSTRAINT `table` FOREIGN KEY (`repo_id`) REFERENCES `repo_table` (`repo_id`)
    case Line of
        "PRIMARY" ++ _ ->
            #table_index{name = "PRIMARY KEY", type = ?INDEX_TYPE_PRIMARY_KEY, sql = Line};
        _ ->
            MP = re_index_pattern(),
            case re:run(Line, MP, [{capture, none}]) of
                match ->
                    case string:tokens(Line, "`") of
                        [_, Name|_] ->
                            #table_index{name = Name,
                                         type = ?INDEX_TYPE_INDEX,
                                         sql = Line};
                        _ ->
                            {error, key_format_error}
                    end;
                nomatch ->
                    %% TODO 暂时不支持外键
                    {error, nomatch}
            end
    end.

re_index_pattern() ->
    case persistent_term:get({?MODULE, re_index_pattern}, undefined) of
        undefined ->
            {ok, MP} = re:compile("^([A-Z]+\s)?KEY\s", []),
            persistent_term:put({?MODULE, re_index_pattern}, MP),
            MP;
        Value ->
            Value
    end.

test_parse_index() ->
    #table_index{} = parse_index("UNIQUE KEY `idx_a` (`a`) USING HASH COMMENT '注释'"),
    #table_index{} = parse_index("FULLTEXT KEY `c` (`c`)"),
    #table_index{} = parse_index("PRIMARY KEY (`d`)"),
    #table_index{} = parse_index("FULLTEXT KEY `c` (`c`)").